﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ProLinq.Projection.Configuration;
using ProLinq.Projection.Visitors;
using ProLinq.Toolkit;
using ProLinq.Tracing;

namespace ProLinq.Projection
{
	/// <summary>
	///     A class where the magic happens
	/// </summary>
	internal class ProjectQueryProvider : QueryProvider, IWrapperProvider
	{
		private readonly ProjectionConfiguration _configuration;
		private readonly IQueryable _source;

		internal ProjectionConfiguration Configuration
		{
			get { return _configuration; }
		}

		public ProjectQueryProvider(IQueryable source, ProjectionConfiguration configuration)
		{
			_configuration = configuration;
			_source = source;
		}

		public override object Execute(Expression expression)
		{
			TraceSources.Projection.TraceEvent(TraceEventType.Information, 0, "Processing expression: {0}", expression);

			// create and configure expression preprocessor
			var preprocessor = new ProjectionVisitor()
				{
					EnableOptimization = _configuration.EnableOptimization,
					FullProjectionRequired = !_configuration.EnablePartialProjection,
					ProjectAllAnonymousTypes = _configuration.ForceAnonymousProjection
				};
			preprocessor.Projections.AddRange(_configuration.Projections);

			// use the preprocessor to get an expression part that can be executed on the source provider
			Expression modifiedExpression = preprocessor.Visit(expression);

			if (preprocessor.ProcessedExpressionPart.Type.IsEnumerable())
			{
				Type elemType = preprocessor.ProcessedExpressionPart.Type.GetEnumerableType();
				IQueryable sourceQuery = _source.Provider.CreateQueryOfT(modifiedExpression);

				TraceSources.Projection.TraceEvent(TraceEventType.Information, 0, "Expression executing on the source: {0}",
				                                   sourceQuery.Expression);

				var mapper = new ProjectionMapper(preprocessor.Projections);
				var typedMappedCollection = ((IEnumerable)mapper.Map(sourceQuery, preprocessor.ProcessedExpressionPart.Type)).AsQueryable();

				// use post processor to create an expression to apply to the result in memory 
				if (preprocessor.ProcessedExpressionPart != expression)
				{
					var postProcessor = new PostProcessingVisitor(preprocessor.ProcessedExpressionPart,
																  typedMappedCollection.Expression);
					Expression finalExpression = postProcessor.Visit(expression);

					TraceSources.Projection.TraceEvent(TraceEventType.Information, 0, "Expression executing in memory: {0}",
					                                   finalExpression);
					if (finalExpression.Type.IsEnumerable())
					{
						return typedMappedCollection.Provider.CreateQueryOfT(finalExpression);
					}
					else
					{
						return typedMappedCollection.Provider.Execute(finalExpression);
					}
				}
				else
				{
					return typedMappedCollection;
				}
			}
			else
			{
				var mapper = new ProjectionMapper(preprocessor.Projections);

				object result = _source.Provider.Execute(modifiedExpression);
				object mappedResult = mapper.Map(result, expression.Type);

				if (preprocessor.ProcessedExpressionPart != expression)
				{
					// We can't get into this branch unless expression is created manually
					// TODO: maybe construct lambda here?
					var postProcessor = new PostProcessingVisitor(preprocessor.ProcessedExpressionPart,
					                                              Expression.Constant(mappedResult));
					Expression finalExpression = postProcessor.Visit(expression);
					throw new NotSupportedException(String.Format("Cannot execute an expression: {0}", finalExpression));
				}
				else
				{
					return mappedResult;
				}
			}
		}

		#region IWrapperProvider Members

		public IQueryable Source
		{
			get { return _source; }
		}

		#endregion
	}
}